name: CODEOWNERS Checks

# Any change in triggers needs to be reflected in the concurrency group.
on:
  pull_request:
    branches:
      - main
      - ft/main/**

permissions: read-all

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  check_changes:
    name: Deduce required tests from code changes
    runs-on: ubuntu-latest
    outputs:
      added-files: ${{ steps.changes.outputs.added-files }}
      deleted-files: ${{ steps.changes.outputs.deleted-files }}
      codeowners-changed: ${{ steps.changes.outputs.codeowners-changed }}
    steps:
      - name: Check code changes
        uses: dorny/paths-filter@ebc4d7e9ebcb0b1eb21480bb8f43113e996ac77a # v3.0.1
        id: changes
        with:
          filters: |
            added-files:
              - added: '**'
            deleted-files:
              - deleted: '**'
            codeowners-changed:
              - 'CODEOWNERS'
              - '.github/workflows/lint-codeowners.yaml'

  codeowners:
    needs: check_changes
    if: ${{ needs.check_changes.outputs.codeowners-changed == 'true' || needs.check_changes.outputs.added-files == 'true' || needs.check_changes.outputs.deleted-files == 'true' }}
    name: Check CODEOWNERS consistency
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          persist-credentials: false
          # Hard-code the path instead of using ${{ github.repository }}
          # to make sure it works for forked repo as well.
          path: src/github.com/cilium/cilium

      - name: Check if all files have attributed code owners
        if: ${{ needs.check_changes.outputs.codeowners-changed == 'true' || needs.check_changes.outputs.added-files == 'true' }}
        run: |
          # CODEOWNERS patterns follows nearly the same syntax as a .gitignore.
          # To check if all files are covered by patterns other than the
          # catch-all '*', we turn the file into a .gitignore and list
          # unmatched files.
          cd src/github.com/cilium/cilium
          # Copy all patterns from CODEOWNERS, but skipping the comments
          # ('^[^#]') and the catch-all '*' rule (the only one with a single
          # character, we skip it with '^.[^ ]').
          awk '/^[^#][^ ]/ {print $1}' CODEOWNERS > .gitignore
          # Reinitialize the repo and list all files NOT covered by .gitignore.
          rm -rf .git
          git init -q
          if [[ -n "$(git ls-files --others -X .gitignore)" ]]; then
              echo '::error title=missing_code_owners::Following files have no owners in CODEOWNERS:'
              git ls-files --others -X .gitignore
              exit 1
          fi

      - name: Check if CODEOWNERS has stale entries
        if: ${{ needs.check_changes.outputs.codeowners-changed == 'true' || needs.check_changes.outputs.deleted-files == 'true' }}
        run: |
          cd src/github.com/cilium/cilium
          EXIT_STATUS=0
          # We go through the patterns in CODEOWNERS, and for each of them we
          # search for corresponding files in the repo.
          while read l; do
              case "${l}" in
                  /*)
                      # The pattern should match from the root of the repo,
                      # we'll use 'ls'. For now, just append pattern to $LIST.
                      LIST+=" ${l#/}"
                      ;;
                  *)
                      # No leading slash: may not be at the root of the repo,
                      # search with 'find'. Print pattern if no file found.
                      if [[ -z $(find . -path "*${l}*" -print -quit) ]]; then
                          echo "${l}"
                          EXIT_STATUS=1
                      fi
                      ;;
              esac
          done <<< $(awk '/^[^#][^ ]/ {print $1}' CODEOWNERS)
          # Just one final call to 'ls' with all /* patterns found. Catch
          # patterns with no corresponding files/directories from stderr.
          STALE_PATTERNS="$(ls -- ${LIST} 2>&1 >/dev/null | sed "s|.*'\(.*\)':.*|/\1|")"
          if [[ -n "${STALE_PATTERNS}" ]]; then
              echo "${STALE_PATTERNS}" | sed 's/ /\n/g'
              EXIT_STATUS=1
          fi
          if [[ ${EXIT_STATUS} -ne 0 ]]; then
              echo '::error title=stale_patterns::The patterns above should be removed from CODEOWNERS.'
              exit ${EXIT_STATUS}
          fi

      - name: Check if all teams in CODEOWNERS rules are documented in the file
        if: ${{ needs.check_changes.outputs.codeowners-changed == 'true' }}
        run: |
          EXIT_STATUS=0
          # List all teams used in CODEOWNERS rules: discard comments and empty
          # lines, discard lines with no team assigned (with no space in it),
          # then discard the first field (pattern to match) for the remaining
          # rules, split the list of teams by replacing spaces with line
          # breaks, sort the results. Then grep for each team name among
          # CODEOWNERS's comments.
          cd src/github.com/cilium/cilium
          for team in $(sed -e '/^\(#\|$\)/d' -e '/^[^ ]*$/d' -e 's/^[^ #]\+ //' -e 's/ /\n/g' CODEOWNERS | sort -u); do
              if ! grep -q "^#[^@]*${team}" CODEOWNERS; then
                  echo "${team}";
                  EXIT_STATUS=1
              fi;
          done
          if [[ ${EXIT_STATUS} -ne 0 ]]; then
              echo '::error title=missing_team::The teams above are not documented in CODEOWNERS. Typo?'
              exit ${EXIT_STATUS}
          fi
